home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / arm / util / sysTools.py < prev    next >
Encoding:
Python Source  |  2012-05-18  |  19.8 KB  |  585 lines

  1. """
  2. Helper functions for working with the underlying system.
  3. """
  4.  
  5. import os
  6. import time
  7. import threading
  8.  
  9. from util import log, procTools, uiTools
  10.  
  11. # Mapping of commands to if they're available or not. This isn't always
  12. # reliable, failing for some special commands. For these the cache is
  13. # prepopulated to skip lookups.
  14. CMD_AVAILABLE_CACHE = {"ulimit": True}
  15.  
  16. # cached system call results, mapping the command issued to the (time, results) tuple
  17. CALL_CACHE = {}
  18. IS_FAILURES_CACHED = True           # caches both successful and failed results if true
  19. CALL_CACHE_LOCK = threading.RLock() # governs concurrent modifications of CALL_CACHE
  20.  
  21. PROCESS_NAME_CACHE = {} # mapping of pids to their process names
  22. PWD_CACHE = {}          # mapping of pids to their present working directory
  23. RESOURCE_TRACKERS = {}  # mapping of pids to their resource tracker instances
  24.  
  25. # Runtimes for system calls, used to estimate cpu usage. Entries are tuples of
  26. # the form:
  27. # (time called, runtime)
  28. RUNTIMES = []
  29. SAMPLING_PERIOD = 5 # time of the sampling period
  30.  
  31. CONFIG = {"queries.resourceUsage.rate": 5,
  32.           "cache.sysCalls.size": 600,
  33.           "log.sysCallMade": log.DEBUG,
  34.           "log.sysCallCached": None,
  35.           "log.sysCallFailed": log.INFO,
  36.           "log.sysCallCacheGrowing": log.INFO,
  37.           "log.stats.failedProcResolution": log.DEBUG,
  38.           "log.stats.procResolutionFailover": log.INFO,
  39.           "log.stats.failedPsResolution": log.INFO}
  40.  
  41. def loadConfig(config):
  42.   config.update(CONFIG)
  43.  
  44. def getSysCpuUsage():
  45.   """
  46.   Provides an estimate of the cpu usage for system calls made through this
  47.   module, based on a sampling period of five seconds. The os.times() function,
  48.   unfortunately, doesn't seem to take popen calls into account. This returns a
  49.   float representing the percentage used.
  50.   """
  51.   
  52.   currentTime = time.time()
  53.   
  54.   # removes any runtimes outside of our sampling period
  55.   while RUNTIMES and currentTime - RUNTIMES[0][0] > SAMPLING_PERIOD:
  56.     RUNTIMES.pop(0)
  57.   
  58.   runtimeSum = sum([entry[1] for entry in RUNTIMES])
  59.   return runtimeSum / SAMPLING_PERIOD
  60.  
  61. def isAvailable(command, cached=True):
  62.   """
  63.   Checks the current PATH to see if a command is available or not. If a full
  64.   call is provided then this just checks the first command (for instance
  65.   "ls -a | grep foo" is truncated to "ls"). This returns True if an accessible
  66.   executable by the name is found and False otherwise.
  67.   
  68.   Arguments:
  69.     command - command for which to search
  70.     cached  - this makes use of available cached results if true, otherwise
  71.               they're overwritten
  72.   """
  73.   
  74.   if " " in command: command = command.split(" ")[0]
  75.   
  76.   if cached and command in CMD_AVAILABLE_CACHE:
  77.     return CMD_AVAILABLE_CACHE[command]
  78.   else:
  79.     cmdExists = False
  80.     for path in os.environ["PATH"].split(os.pathsep):
  81.       cmdPath = os.path.join(path, command)
  82.       
  83.       if os.path.exists(cmdPath) and os.access(cmdPath, os.X_OK):
  84.         cmdExists = True
  85.         break
  86.     
  87.     CMD_AVAILABLE_CACHE[command] = cmdExists
  88.     return cmdExists
  89.  
  90. def getFileErrorMsg(exc):
  91.   """
  92.   Strips off the error number prefix for file related IOError messages. For
  93.   instance, instead of saying:
  94.   [Errno 2] No such file or directory
  95.   
  96.   this would return:
  97.   no such file or directory
  98.   
  99.   Arguments:
  100.     exc - file related IOError exception
  101.   """
  102.   
  103.   excStr = str(exc)
  104.   if excStr.startswith("[Errno ") and "] " in excStr:
  105.     excStr = excStr[excStr.find("] ") + 2:].strip()
  106.     excStr = excStr[0].lower() + excStr[1:]
  107.   
  108.   return excStr
  109.  
  110. def getProcessName(pid, default = None, cacheFailure = True):
  111.   """
  112.   Provides the name associated with the given process id. This isn't available
  113.   on all platforms.
  114.   
  115.   Arguments:
  116.     pid          - process id for the process being returned
  117.     default      - result if the process name can't be retrieved (raises an
  118.                    IOError on failure instead if undefined)
  119.     cacheFailure - if the lookup fails and there's a default then caches the
  120.                    default value to prevent further lookups
  121.   """
  122.   
  123.   if pid in PROCESS_NAME_CACHE:
  124.     return PROCESS_NAME_CACHE[pid]
  125.   
  126.   processName, raisedExc = "", None
  127.   
  128.   # fetch it from proc contents if available
  129.   if procTools.isProcAvailable():
  130.     try:
  131.       processName = procTools.getStats(pid, procTools.Stat.COMMAND)[0]
  132.     except IOError, exc:
  133.       raisedExc = exc
  134.   
  135.   # fall back to querying via ps
  136.   if not processName:
  137.     # the ps call formats results as:
  138.     # COMMAND
  139.     # tor
  140.     psCall = call("ps -p %s -o command" % pid)
  141.     
  142.     if psCall and len(psCall) >= 2 and not " " in psCall[1]:
  143.       processName, raisedExc = psCall[1].strip(), None
  144.     else:
  145.       raisedExc = ValueError("Unexpected output from ps: %s" % psCall)
  146.   
  147.   if raisedExc:
  148.     if default == None: raise raisedExc
  149.     else:
  150.       if cacheFailure:
  151.         PROCESS_NAME_CACHE[pid] = default
  152.       
  153.       return default
  154.   else:
  155.     processName = os.path.basename(processName)
  156.     PROCESS_NAME_CACHE[pid] = processName
  157.     return processName
  158.  
  159. def getPwd(pid):
  160.   """
  161.   Provices the working directory of the given process. This raises an IOError
  162.   if it can't be determined.
  163.   
  164.   Arguments:
  165.     pid - pid of the process
  166.   """
  167.   
  168.   if not pid: raise IOError("we couldn't get the pid")
  169.   elif pid in PWD_CACHE: return PWD_CACHE[pid]
  170.   
  171.   # try fetching via the proc contents if available
  172.   if procTools.isProcAvailable():
  173.     try:
  174.       pwd = procTools.getPwd(pid)
  175.       PWD_CACHE[pid] = pwd
  176.       return pwd
  177.     except IOError: pass # fall back to pwdx
  178.   elif os.uname()[0] in ("Darwin", "FreeBSD", "OpenBSD"):
  179.     # BSD neither useres the above proc info nor does it have pwdx. Use lsof to
  180.     # determine this instead:
  181.     # https://trac.torproject.org/projects/tor/ticket/4236
  182.     #
  183.     # ~$ lsof -a -p 75717 -d cwd -Fn
  184.     # p75717
  185.     # n/Users/atagar/tor/src/or
  186.     
  187.     try:
  188.       results = call("lsof -a -p %s -d cwd -Fn" % pid)
  189.       
  190.       if results and len(results) == 2 and results[1].startswith("n/"):
  191.         pwd = results[1][1:].strip()
  192.         PWD_CACHE[pid] = pwd
  193.         return pwd
  194.     except IOError, exc: pass
  195.   
  196.   try:
  197.     # pwdx results are of the form:
  198.     # 3799: /home/atagar
  199.     # 5839: No such process
  200.     results = call("pwdx %s" % pid)
  201.     if not results:
  202.       raise IOError("pwdx didn't return any results")
  203.     elif results[0].endswith("No such process"):
  204.       raise IOError("pwdx reported no process for pid " + pid)
  205.     elif len(results) != 1 or results[0].count(" ") != 1:
  206.       raise IOError("we got unexpected output from pwdx")
  207.     else:
  208.       pwd = results[0][results[0].find(" ") + 1:].strip()
  209.       PWD_CACHE[pid] = pwd
  210.       return pwd
  211.   except IOError, exc:
  212.     raise IOError("the pwdx call failed: " + str(exc))
  213.  
  214. def expandRelativePath(path, ownerPid):
  215.   """
  216.   Expands relative paths to be an absolute path with reference to a given
  217.   process. This raises an IOError if the process pwd is required and can't be
  218.   resolved.
  219.   
  220.   Arguments:
  221.     path     - path to be expanded
  222.     ownerPid - pid of the process to which the path belongs
  223.   """
  224.   
  225.   if not path or path[0] == "/": return path
  226.   else:
  227.     if path.startswith("./"): path = path[2:]
  228.     processPwd = getPwd(ownerPid)
  229.     return "%s/%s" % (processPwd, path)
  230.  
  231. def call(command, cacheAge=0, suppressExc=False, quiet=True):
  232.   """
  233.   Convenience function for performing system calls, providing:
  234.   - suppression of any writing to stdout, both directing stderr to /dev/null
  235.     and checking for the existence of commands before executing them
  236.   - logging of results (command issued, runtime, success/failure, etc)
  237.   - optional exception suppression and caching (the max age for cached results
  238.     is a minute)
  239.   
  240.   Arguments:
  241.     command     - command to be issued
  242.     cacheAge    - uses cached results rather than issuing a new request if last
  243.                   fetched within this number of seconds (if zero then all
  244.                   caching functionality is skipped)
  245.     suppressExc - provides None in cases of failure if True, otherwise IOErrors
  246.                   are raised
  247.     quiet       - if True, "2> /dev/null" is appended to all commands
  248.   """
  249.   
  250.   # caching functionality (fetching and trimming)
  251.   if cacheAge > 0:
  252.     global CALL_CACHE
  253.     
  254.     # keeps consistency that we never use entries over a minute old (these
  255.     # results are 'dirty' and might be trimmed at any time)
  256.     cacheAge = min(cacheAge, 60)
  257.     cacheSize = CONFIG["cache.sysCalls.size"]
  258.     
  259.     # if the cache is especially large then trim old entries
  260.     if len(CALL_CACHE) > cacheSize:
  261.       CALL_CACHE_LOCK.acquire()
  262.       
  263.       # checks that we haven't trimmed while waiting
  264.       if len(CALL_CACHE) > cacheSize:
  265.         # constructs a new cache with only entries less than a minute old
  266.         newCache, currentTime = {}, time.time()
  267.         
  268.         for cachedCommand, cachedResult in CALL_CACHE.items():
  269.           if currentTime - cachedResult[0] < 60:
  270.             newCache[cachedCommand] = cachedResult
  271.         
  272.         # if the cache is almost as big as the trim size then we risk doing this
  273.         # frequently, so grow it and log
  274.         if len(newCache) > (0.75 * cacheSize):
  275.           cacheSize = len(newCache) * 2
  276.           CONFIG["cache.sysCalls.size"] = cacheSize
  277.           
  278.           msg = "growing system call cache to %i entries" % cacheSize
  279.           log.log(CONFIG["log.sysCallCacheGrowing"], msg)
  280.         
  281.         CALL_CACHE = newCache
  282.       CALL_CACHE_LOCK.release()
  283.     
  284.     # checks if we can make use of cached results
  285.     if command in CALL_CACHE and time.time() - CALL_CACHE[command][0] < cacheAge:
  286.       cachedResults = CALL_CACHE[command][1]
  287.       cacheAge = time.time() - CALL_CACHE[command][0]
  288.       
  289.       if isinstance(cachedResults, IOError):
  290.         if IS_FAILURES_CACHED:
  291.           msg = "system call (cached failure): %s (age: %0.1f, error: %s)" % (command, cacheAge, str(cachedResults))
  292.           log.log(CONFIG["log.sysCallCached"], msg)
  293.           
  294.           if suppressExc: return None
  295.           else: raise cachedResults
  296.         else:
  297.           # flag was toggled after a failure was cached - reissue call, ignoring the cache
  298.           return call(command, 0, suppressExc, quiet)
  299.       else:
  300.         msg = "system call (cached): %s (age: %0.1f)" % (command, cacheAge)
  301.         log.log(CONFIG["log.sysCallCached"], msg)
  302.         
  303.         return cachedResults
  304.   
  305.   startTime = time.time()
  306.   commandCall, results, errorExc = None, None, None
  307.   
  308.   # Gets all the commands involved, taking piping into consideration. If the
  309.   # pipe is quoted (ie, echo "an | example") then it's ignored.
  310.   
  311.   commandComp = []
  312.   for component in command.split("|"):
  313.     if not commandComp or component.count("\"") % 2 == 0:
  314.       commandComp.append(component)
  315.     else:
  316.       # pipe is within quotes
  317.       commandComp[-1] += "|" + component
  318.   
  319.   # preprocessing for the commands to prevent anything going to stdout
  320.   for i in range(len(commandComp)):
  321.     subcommand = commandComp[i].strip()
  322.     
  323.     if not isAvailable(subcommand): errorExc = IOError("'%s' is unavailable" % subcommand.split(" ")[0])
  324.     if quiet: commandComp[i] = "%s 2> /dev/null" % subcommand
  325.   
  326.   # processes the system call
  327.   if not errorExc:
  328.     try:
  329.       commandCall = os.popen(" | ".join(commandComp))
  330.       results = commandCall.readlines()
  331.     except IOError, exc:
  332.       errorExc = exc
  333.   
  334.   # make sure sys call is closed
  335.   if commandCall: commandCall.close()
  336.   
  337.   if errorExc:
  338.     # log failure and either provide None or re-raise exception
  339.     msg = "system call (failed): %s (error: %s)" % (command, str(errorExc))
  340.     log.log(CONFIG["log.sysCallFailed"], msg)
  341.     
  342.     if cacheAge > 0 and IS_FAILURES_CACHED:
  343.       CALL_CACHE_LOCK.acquire()
  344.       CALL_CACHE[command] = (time.time(), errorExc)
  345.       CALL_CACHE_LOCK.release()
  346.     
  347.     if suppressExc: return None
  348.     else: raise errorExc
  349.   else:
  350.     # log call information and if we're caching then save the results
  351.     currentTime = time.time()
  352.     runtime = currentTime - startTime
  353.     msg = "system call: %s (runtime: %0.2f)" % (command, runtime)
  354.     log.log(CONFIG["log.sysCallMade"], msg)
  355.     
  356.     # append the runtime, and remove any outside of the sampling period
  357.     RUNTIMES.append((currentTime, runtime))
  358.     while RUNTIMES and currentTime - RUNTIMES[0][0] > SAMPLING_PERIOD:
  359.       RUNTIMES.pop(0)
  360.     
  361.     if cacheAge > 0:
  362.       CALL_CACHE_LOCK.acquire()
  363.       CALL_CACHE[command] = (time.time(), results)
  364.       CALL_CACHE_LOCK.release()
  365.     
  366.     return results
  367.  
  368. def getResourceTracker(pid, noSpawn = False):
  369.   """
  370.   Provides a running singleton ResourceTracker instance for the given pid.
  371.   
  372.   Arguments:
  373.     pid     - pid of the process being tracked
  374.     noSpawn - returns None rather than generating a singleton instance if True
  375.   """
  376.   
  377.   if pid in RESOURCE_TRACKERS:
  378.     tracker = RESOURCE_TRACKERS[pid]
  379.     if tracker.isAlive(): return tracker
  380.     else: del RESOURCE_TRACKERS[pid]
  381.   
  382.   if noSpawn: return None
  383.   tracker = ResourceTracker(pid, CONFIG["queries.resourceUsage.rate"])
  384.   RESOURCE_TRACKERS[pid] = tracker
  385.   tracker.start()
  386.   return tracker
  387.  
  388. class ResourceTracker(threading.Thread):
  389.   """
  390.   Periodically fetches the resource usage (cpu and memory usage) for a given
  391.   process.
  392.   """
  393.   
  394.   def __init__(self, processPid, resolveRate):
  395.     """
  396.     Initializes a new resolver daemon. When no longer needed it's suggested
  397.     that this is stopped.
  398.     
  399.     Arguments:
  400.       processPid  - pid of the process being tracked
  401.       resolveRate - time between resolving resource usage, resolution is
  402.                     disabled if zero
  403.     """
  404.     
  405.     threading.Thread.__init__(self)
  406.     self.setDaemon(True)
  407.     
  408.     self.processPid = processPid
  409.     self.resolveRate = resolveRate
  410.     
  411.     self.cpuSampling = 0.0  # latest cpu usage sampling
  412.     self.cpuAvg = 0.0       # total average cpu usage
  413.     self.memUsage = 0       # last sampled memory usage in bytes
  414.     self.memUsagePercentage = 0.0 # percentage cpu usage
  415.     
  416.     # resolves usage via proc results if true, ps otherwise
  417.     self._useProc = procTools.isProcAvailable()
  418.     
  419.     # used to get the deltas when querying cpu time
  420.     self._lastCpuTotal = 0
  421.     
  422.     self.lastLookup = -1
  423.     self._halt = False      # terminates thread if true
  424.     self._valLock = threading.RLock()
  425.     self._cond = threading.Condition()  # used for pausing the thread
  426.     
  427.     # number of successful calls we've made
  428.     self._runCount = 0
  429.     
  430.     # sequential times we've failed with this method of resolution
  431.     self._failureCount = 0
  432.   
  433.   def getResourceUsage(self):
  434.     """
  435.     Provides the last cached resource usage as a tuple of the form:
  436.     (cpuUsage_sampling, cpuUsage_avg, memUsage_bytes, memUsage_percent)
  437.     """
  438.     
  439.     self._valLock.acquire()
  440.     results = (self.cpuSampling, self.cpuAvg, self.memUsage, self.memUsagePercentage)
  441.     self._valLock.release()
  442.     
  443.     return results
  444.   
  445.   def getRunCount(self):
  446.     """
  447.     Provides the number of times we've successfully fetched the resource
  448.     usages.
  449.     """
  450.     
  451.     return self._runCount
  452.   
  453.   def lastQueryFailed(self):
  454.     """
  455.     Provides true if, since we fetched the currently cached results, we've
  456.     failed to get new results. False otherwise.
  457.     """
  458.     
  459.     return self._failureCount != 0
  460.   
  461.   def run(self):
  462.     while not self._halt:
  463.       timeSinceReset = time.time() - self.lastLookup
  464.       
  465.       if self.resolveRate == 0:
  466.         self._cond.acquire()
  467.         if not self._halt: self._cond.wait(0.2)
  468.         self._cond.release()
  469.         
  470.         continue
  471.       elif timeSinceReset < self.resolveRate:
  472.         sleepTime = max(0.2, self.resolveRate - timeSinceReset)
  473.         
  474.         self._cond.acquire()
  475.         if not self._halt: self._cond.wait(sleepTime)
  476.         self._cond.release()
  477.         
  478.         continue # done waiting, try again
  479.       
  480.       newValues = {}
  481.       try:
  482.         if self._useProc:
  483.           utime, stime, startTime = procTools.getStats(self.processPid, procTools.Stat.CPU_UTIME, procTools.Stat.CPU_STIME, procTools.Stat.START_TIME)
  484.           totalCpuTime = float(utime) + float(stime)
  485.           cpuDelta = totalCpuTime - self._lastCpuTotal
  486.           newValues["cpuSampling"] = cpuDelta / timeSinceReset
  487.           newValues["cpuAvg"] = totalCpuTime / (time.time() - float(startTime))
  488.           newValues["_lastCpuTotal"] = totalCpuTime
  489.           
  490.           memUsage = int(procTools.getMemoryUsage(self.processPid)[0])
  491.           totalMemory = procTools.getPhysicalMemory()
  492.           newValues["memUsage"] = memUsage
  493.           newValues["memUsagePercentage"] = float(memUsage) / totalMemory
  494.         else:
  495.           # the ps call formats results as:
  496.           # 
  497.           #     TIME     ELAPSED   RSS %MEM
  498.           # 3-08:06:32 21-00:00:12 121844 23.5
  499.           # 
  500.           # or if Tor has only recently been started:
  501.           # 
  502.           #     TIME      ELAPSED    RSS %MEM
  503.           #  0:04.40        37:57  18772  0.9
  504.           
  505.           psCall = call("ps -p %s -o cputime,etime,rss,%%mem" % self.processPid)
  506.           
  507.           isSuccessful = False
  508.           if psCall and len(psCall) >= 2:
  509.             stats = psCall[1].strip().split()
  510.             
  511.             if len(stats) == 4:
  512.               try:
  513.                 totalCpuTime = uiTools.parseShortTimeLabel(stats[0])
  514.                 uptime = uiTools.parseShortTimeLabel(stats[1])
  515.                 cpuDelta = totalCpuTime - self._lastCpuTotal
  516.                 newValues["cpuSampling"] = cpuDelta / timeSinceReset
  517.                 newValues["cpuAvg"] = totalCpuTime / uptime
  518.                 newValues["_lastCpuTotal"] = totalCpuTime
  519.                 
  520.                 newValues["memUsage"] = int(stats[2]) * 1024 # ps size is in kb
  521.                 newValues["memUsagePercentage"] = float(stats[3]) / 100.0
  522.                 isSuccessful = True
  523.               except ValueError, exc: pass
  524.           
  525.           if not isSuccessful:
  526.             raise IOError("unrecognized output from ps: %s" % psCall)
  527.       except IOError, exc:
  528.         newValues = {}
  529.         self._failureCount += 1
  530.         
  531.         if self._useProc:
  532.           if self._failureCount >= 3:
  533.             # We've failed three times resolving via proc. Warn, and fall back
  534.             # to ps resolutions.
  535.             msg = "Failed three attempts to get process resource usage from proc, falling back to ps (%s)" % exc
  536.             log.log(CONFIG["log.stats.procResolutionFailover"], msg)
  537.             
  538.             self._useProc = False
  539.             self._failureCount = 1 # prevents lastQueryFailed() from thinking that we succeeded
  540.           else:
  541.             # wait a bit and try again
  542.             msg = "Unable to query process resource usage from proc (%s)" % exc
  543.             log.log(CONFIG["log.stats.failedProcResolution"], msg)
  544.             self._cond.acquire()
  545.             if not self._halt: self._cond.wait(0.5)
  546.             self._cond.release()
  547.         else:
  548.           # exponential backoff on making failed ps calls
  549.           sleepTime = 0.01 * (2 ** self._failureCount) + self._failureCount
  550.           msg = "Unable to query process resource usage from ps, waiting %0.2f seconds (%s)" % (sleepTime, exc)
  551.           log.log(CONFIG["log.stats.failedProcResolution"], msg)
  552.           self._cond.acquire()
  553.           if not self._halt: self._cond.wait(sleepTime)
  554.           self._cond.release()
  555.       
  556.       # sets the new values
  557.       if newValues:
  558.         # If this is the first run then the cpuSampling stat is meaningless
  559.         # (there isn't a previous tick to sample from so it's zero at this
  560.         # point). Setting it to the average, which is a fairer estimate.
  561.         if self.lastLookup == -1:
  562.           newValues["cpuSampling"] = newValues["cpuAvg"]
  563.         
  564.         self._valLock.acquire()
  565.         self.cpuSampling = newValues["cpuSampling"]
  566.         self.cpuAvg = newValues["cpuAvg"]
  567.         self.memUsage = newValues["memUsage"]
  568.         self.memUsagePercentage = newValues["memUsagePercentage"]
  569.         self._lastCpuTotal = newValues["_lastCpuTotal"]
  570.         self.lastLookup = time.time()
  571.         self._runCount += 1
  572.         self._failureCount = 0
  573.         self._valLock.release()
  574.   
  575.   def stop(self):
  576.     """
  577.     Halts further resolutions and terminates the thread.
  578.     """
  579.     
  580.     self._cond.acquire()
  581.     self._halt = True
  582.     self._cond.notifyAll()
  583.     self._cond.release()
  584.  
  585.